<?php

/**
 * mForge Framework
 *
 * LICENSE:
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * The license is bundled with this package in the file LICENSE.txt.
 * You may also obtain a copy of the License at this URL:
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @copyright  Copyright (c) 2011, Jonathan Hedrén
 * @author     Jonathan Hedrén <jonathan.hedren@gmail.com>
 * @license    http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
 */

namespace mForge;

/**
 * Parses DocComments via reflection to simulate attributes/annotations.
 * 
 * [MyAttribute]
 * [MyAttribute('test', "test2", MY_CONSTANT, MyClass::CONSTANT, 1.2515, 5)]
 * [MyAttribute(false, true)]
 * [MyAttribute(false, { 'item1', "item2", false })];
 * [MyAttribute(key = { 'item1', "item2", false })];
 * [MyAttribute({ key = 'value', key2 = true }, 'b-value', property = 'value')]
 */
class AttributeParser
{
    const STATE_CLASS = 1;
    const STATE_VALUES = 2;
    const STATE_STRING_VALUE_1 = 3;
    const STATE_STRING_VALUE_2 = 4;
    const STATE_ARRAY_VALUE = 5;

    /**
     *
     * @var array
     */
    private static $attributesCache = array();

    /**
     *
     * @var array
     */
    private static $attributeAdapters = array(
        'Description' => '\\mForge\\DescriptionAttribute',
        'DisplayName' => '\\mForge\\DisplayNameAttribute',
        'EmailValid' => '\\mForge\\EmailValidAttribute',
        'HtmlEscape' => '\\mForge\\HtmlEscapeAttribute',
        'IpValid' => '\\mForge\\IpValidAttribute',
        'Numeric' => '\\mForge\\NumericAttribute',
        'Required' => '\\mForge\\RequiredAttribute',
        'StringLengthAttribute' => '\\mForge\\StringLengthAttribute',
        'StripTags' => '\\mForge\\StripTagsAttribute',
        'Trim' => '\\mForge\\TrimAttribute',
        'UrlEncode' => '\\mForge\\UrlEncodeAttribute',
        'UrlValid' => '\\mForge\\UrlValidAttribute'
    );

    /**
     * Returns any attributes found in the class comments.
     *
     * @param string|object $classOrInstance
     * @return array
     */
    public static function getClassAttributesFor($classOrInstance)
    {
        $class = is_object($classOrInstance) ? get_class($classOrInstance) : $classOrInstance;

        if (isset(self::$attributesCache[$class])) {
            return self::$attributesCache[$class];
        }

        $reflection = new \ReflectionClass($classOrInstance);
        $attributes = self::parse($reflection->getDocComment());

        self::$attributesCache[$class] = $attributes;

        return $attributes;
    }

    /**
     * Returns any attributes found in the supplied method's comments.
     *
     * @param string|object $classOrInstance
     * @param string $method
     * @return array
     */
    public static function getMethodAttributesFor($classOrInstance, $method)
    {
        $class = is_object($classOrInstance) ? get_class($classOrInstance) : $classOrInstance;
        $cacheKey = "$class::$method";

        if (isset(self::$attributesCache[$cacheKey])) {
            return self::$attributesCache[$cacheKey];
        }

        $reflection = new \ReflectionClass($classOrInstance);
        $reflectionMethod = $reflection->getMethod($method);

        $attributes = self::parse($reflectionMethod->getDocComment());

        self::$attributesCache[$cacheKey] = $attributes;

        return $attributes;
    }


    /**
     * Returns any attributes found in the supplied property's comments.
     *
     * @param string|object $classOrInstance
     * @param string $method
     * @return array
     */
    public static function getPropertyAttributesFor($classOrInstance, $property)
    {

        $class = is_object($classOrInstance) ? get_class($classOrInstance) : $classOrInstance;
        $cacheKey = "$class->$property";

        if (isset(self::$attributesCache[$cacheKey])) {
            return self::$attributesCache[$cacheKey];
        }

        $reflection = new \ReflectionClass($class);
        $reflectionProperty = $reflection->getProperty($property);

        $attributes = self::parse($reflectionProperty->getDocComment());

        self::$attributesCache[$cacheKey] = $attributes;

        return $attributes;
    }


    /**
     *
     * @param string $attributeName
     * @param string $attributeClass
     */
    public static function setAttributeAdapter($attributeName, $attributeClass)
    {
        self::$attributeAdapters[$attributeName] = $attributeClass;
    }

    /**
     *
     * @todo this method really needs to be refactored
     * @param string $docComment
     * @return array
     */
    private static function parse($docComment)
    {
        $startPos = strpos($docComment, '[');
        $endPos = strrpos($docComment, ']');
        if ($startPos === false || $endPos === false) {
            return null;
        }
        $docComment = substr($docComment, $startPos, $endPos);

        $attributes = array();

        $state = null;
        $valuesState = null;

        $attributeName = null;
        $constructorValues = array();
        $propertyValues = array();
        $tmpArrayValues = array();

        $token = null;

        $escapeNextChar = false;

        $propertyKey = null;
        $arrayKey = null;
       
        $i = 0;
        $docCommentLength = strlen($docComment);
        do {
            //$char = current($docCommentAsArray);
            $char = $docComment[$i];
            switch ($char) {
                case '[':
                    if ($state === null) {
                        $state = self::STATE_CLASS;
                        $token = null;
                        continue 2;
                    }
                    break;
                case '(':
                    if ($state == self::STATE_CLASS) {
                        // attribute values start
                        $state = self::STATE_VALUES;
                        $valuesState = $state;
                        $attributeName = $token;
                        $token = null;
                        continue 2;
                    }
                    break;
                case "'":
                    if ($token === null) {
                        $state = self::STATE_STRING_VALUE_1;
                        continue 2;
                    } else if ($state == self::STATE_STRING_VALUE_1 && !$escapeNextChar) {
                        if ($valuesState == self::STATE_ARRAY_VALUE) {
                            if ($arrayKey == null) {
                                $tmpArrayValues[] = $token;
                            } else {
                                $tmpArrayValues[$arrayKey] = $token;
                                $arrayKey = null;
                            }
                        } else {
                            if ($propertyKey == null) {
                                $constructorValues[] = $token;
                            } else {
                                $propertyValues[$propertyKey] = $token;
                                $arrayKey = null;
                            }
                        }
                        $token = null;
                        $state = $valuesState;
                        continue 2;
                    }
                    break;
                case '"':
                    // start/end of string value
                    if ($token === null) {
                        $state = self::STATE_STRING_VALUE_2;
                        continue 2;
                    } else if ($state == self::STATE_STRING_VALUE_2 && !$escapeNextChar) {
                        if ($valuesState == self::STATE_ARRAY_VALUE) {
                            if ($arrayKey == null) {
                                $tmpArrayValues[] = $token;
                            } else {
                                $tmpArrayValues[$arrayKey] = $token;
                                $arrayKey = null;
                            }
                        } else {
                            if ($propertyKey == null) {
                                $constructorValues[] = $token;
                            } else {
                                $propertyValues[$propertyKey] = $token;
                                $propertyKey = null;
                            }
                        }
                        $token = null;
                        $state = $valuesState;
                        continue 2;
                    }
                    break;
                case ',':
                    if ($state == self::STATE_VALUES || $state == self::STATE_ARRAY_VALUE) {
                        if ($token !== null) {
                            $token = self::evaluateToken($token);
                            if ($valuesState == self::STATE_ARRAY_VALUE) {
                                if ($arrayKey == null) {
                                    $tmpArrayValues[] = $token;
                                } else {
                                    $tmpArrayValues[$arrayKey] = $token;
                                    $arrayKey = null;
                                }
                            } else {
                                if ($propertyKey == null) {
                                    $constructorValues[] = $token;
                                } else {
                                    $propertyValues[$propertyKey] = $token;
                                    $propertyKey = null;
                                }
                            }
                            $token = null;
                        }
                        continue 2;
                    } else if ($state == self::STATE_CLASS) {
                        if ($attributeName === null) {
                            $attributeName = $token;
                            $token = null;
                        }
                        if (isset(self::$attributeAdapters[$attributeName])) {
                            $attributeClass = self::$attributeAdapters[$attributeName];
                            if (!class_exists($attributeClass)) {
                                continue 2;
                            }

                            $attributes[] = self::createAttribute($attributeClass, $constructorValues, $propertyValues);
                        }

                        $attributeName = null;
                        $constructorValues = array();
                        $propertyValues = array();
                        $tmpArrayValues = array();
                        continue 2;
                    }
                    break;
                case '\\':
                    if (!$escapeNextChar) {
                        $escapeNextChar = true;
                        continue 2;
                    }
                    break;
                case '=':
                    if ($state == self::STATE_VALUES || $state == self::STATE_ARRAY_VALUE) {
                        if ($valuesState == self::STATE_ARRAY_VALUE) {
                            $arrayKey = $token;
                        } else {
                            $propertyKey = $token;
                        }
                        $token = null;
                        continue 2;
                    }
                    break;
                case '{':
                    // array start
                    if ($state == self::STATE_VALUES) {
                        $state = self::STATE_ARRAY_VALUE;
                        $valuesState = $state;
                        $tmpArrayValues = array();
                        $token = null;
                        continue 2;
                    }
                    break;
                case '}':
                    if ($state == self::STATE_ARRAY_VALUE) {
                        $state = self::STATE_VALUES;
                        $valuesState = $state;
                        if ($token !== null) {
                            $token = self::evaluateToken($token);
                            if ($arrayKey == null) {
                                $tmpArrayValues[] = $token;
                            } else {
                                $tmpArrayValues[$arrayKey] = $token;
                                $arrayKey = null;
                            }
                            $token = null;
                        }
                        if ($propertyKey == null) {
                            $constructorValues[] = $tmpArrayValues;
                        } else {
                            $propertyValues[$propertyKey] = $tmpArrayValues;
                        }
                        $tmpArrayValues = array();
                        $token = null;
                        continue 2;
                    }
                    // array end
                    break;
                case ')':
                    // attributes values end
                    if ($state == self::STATE_VALUES) {
                        $state = self::STATE_CLASS;
                        if ($token !== null) {
                            $token = self::evaluateToken($token);
                            if ($propertyKey == null) {
                                $constructorValues[] = $token;
                            } else {
                                $propertyValues[$propertyKey] = $token;
                                $propertyKey = null;
                            }
                            $token = null;
                        }
                        $constructorValues = $constructorValues + $tmpArrayValues;
                        $tmpArrayValues = array();
                        continue 2;
                    }
                    break;
                case ']':
                    if ($state == self::STATE_CLASS) {
                        if ($attributeName === null) {
                            $attributeName = $token;
                            $token = null;
                        }
                        if (isset(self::$attributeAdapters[$attributeName])) {
                            $attributeClass = self::$attributeAdapters[$attributeName];
                            
                            if (!class_exists($attributeClass)) {
                                continue 2;
                            }

                            $attributes[] = self::createAttribute($attributeClass, $constructorValues, $propertyValues);
                        }
                        
                        $attributeName = null;
                        $constructorValues = array();
                        $propertyValues = array();
                        $tmpArrayValues = array();
                        $state = null;
                        continue 2;
                    }
                    break;
                case ' ':
                    if ($state == self::STATE_CLASS) {
                        $token = null;
                        continue 2;
                    } else if ($state == self::STATE_VALUES || $state == self::STATE_ARRAY_VALUE) {
                        continue 2;
                    }
            }
            $token .= $char;
            $escapeNextChar = false;
        } while (++$i < $docCommentLength);

        return count($attributes) ? $attributes : null;
    }

    /**
     *
     * @param mixed $token
     * @return mixed
     */
    private static function evaluateToken($token)
    {
        if ($token == 'false') {
            return false;
        }
        if ($token == 'true') {
            return true;
        }
        if ($token == 'null' || $token == 'NULL') {
            return null;
        }
        if (is_numeric($token)) {
            return $token;
        }
        return constant($token);
    }

    /**
     * Creates an attribute instance from the supplied parameters.
     *
     * @param string $attributeClass
     * @param array $constructorValues
     * @param array $propertyValues
     * @return mixed
     */
    private static function createAttribute($attributeClass, array $constructorValues, array $propertyValues)
    {
        if (count($constructorValues)) {
            $reflection = new \ReflectionClass($attributeClass);
            // Todo: om namngivet värde, $instance->$key = $value
            $attribute = $reflection->newInstanceArgs($constructorValues);
        } else {
            $attribute = new $attributeClass();
        }
        foreach ($propertyValues as $property => $value) {
            $attribute->$property = $value;
        }

        return $attribute;
    }
}